package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpStatus;
import com.aliyun.dm20151123.models.SingleSendMailResponse;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    private final StringRedisTemplate stringRedisTemplate ;
    private final SendMessageCode sendMessageCode ;
    private final JwtUtils jwtUtils ;

    /**
     * 发送验证码
     *
     * @param phone
     * @param session
     * @return
     */
    public String sendCode(String phone, HttpSession session) throws Exception {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone) && RegexUtils.isEmailInvalid(phone)) {
            // 2.如果不符合 返回错误信息
            throw new RuntimeException("手机号格式错误") ;
        }
        // 3.符合 生成六位验证码
        String code = RandomUtil.randomNumbers(SystemConstants.VERIFY_CODE_LENGTH);
        // 4.保存验证码到 Redis
        String key = RedisConstants.LOGIN_CODE_KEY + phone;
        stringRedisTemplate.opsForValue().set(key,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 5.发送验证码
        SingleSendMailResponse response = sendMessageCode.sendMail(phone, code);// 发送验证码
        if (response.getStatusCode().equals(HttpStatus.HTTP_OK)) {
            log.debug("发送短信验证码成功，验证码：{}", code);
        } else {
            log.debug("发送短信验证码失败...");
            throw new RuntimeException("发送短信验证码失败");
        }
        // 6.返回
        return code;
    }

    /**
     * 用户登录
     *
     * @param loginForm
     * @param session
     */
    @Transactional  // 有时候写入数据库失败但是添加 Redis 成功了 就造成了误认为登录了的场景
    public String login(LoginFormDTO loginForm, HttpSession session) {
        // 判断用户是否登录过 (单点登录)
        String user_String = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_USER_KEY + loginForm.getPhone());
        if (StrUtil.isNotBlank(user_String)) {
            // 登录过
            log.info("用户已登录 {}",user_String);
            throw new RuntimeException("用户已登录") ;
        }
        // 判断是不是密码登录
        if (loginForm.getPassword() != null) {
            // 密码登录
            return this.loginWithPassword(loginForm) ;
        }
        // 1.校验手机号
        String phone = loginForm.getPhone() ;
        if (RegexUtils.isPhoneInvalid(phone) && RegexUtils.isEmailInvalid(phone)) {
            // 手机号格式错误
            throw new RuntimeException("手机号格式错误") ;
        }
        // 2.校验验证码
        // 2.1 获取 Redis 中的验证码
        String key = RedisConstants.LOGIN_CODE_KEY + phone;
        String cacheCode = stringRedisTemplate.opsForValue().get(key) ;
        if (cacheCode == null) {
            // 没有发送过验证码
            throw new RuntimeException("请先点击发送验证码") ;
        }
        // 2.2 获取用户提交的验证码
        String code = loginForm.getCode();
        if (RegexUtils.isCodeInvalid(code)) {
            // 验证码格式错误
            throw new RuntimeException("验证码格式错误") ;
        }
        if (!cacheCode.equals(code)) {
            // 3.不一致 返回错误信息
            throw new RuntimeException("验证码错误") ;
        }
        // 4.一致 根据手机号查询用户
        User user = this.query()
                .eq("phone", phone)
                .one();
        if (user == null) {
            // 用户不存在 创建用户并保存到数据库 (自动注册)
            user = this.createUserWithPhone(phone) ;
        }
        // 5.将用户保存到 Redis 中
        // 没登陆过就存 phone
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_USER_KEY + loginForm.getPhone(),loginForm.getPhone(),RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
        // 获取 jwt 令牌
        String jwt = jwtUtils.generateJwt(user);
        // 转换为 map 便于存储
        Map<String, Object> userMap = BeanUtil.beanToMap(user,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((field,value)->value.toString()));
        key = RedisConstants.LOGIN_USER_KEY + jwt;
        stringRedisTemplate.opsForHash().putAll(key,userMap);
        // 添加过期时间
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES) ;
        log.debug("用户登录成功 {}",user);
        // 返回 jwt 令牌
        return jwt;
    }

    /**
     * 用户退出
     *
     * @param session
     */
    @Override
    @Transactional
    public void logout(HttpServletRequest request ,HttpSession session) {
        // 获取用户
        UserDTO user = UserHolder.getUser();
        // 获取电话
        String phone = user.getPhone() ;
        // 获取 jwt
        String jwt = request.getHeader("authorization");
        // 获取 key
        String key = RedisConstants.LOGIN_USER_KEY + jwt;
        // 移除 session 中的用户
        session.removeAttribute(key);
        // 移除 Redis 中的用户
        stringRedisTemplate.delete(key);
        // 移除 Redis 中用于单点登录的用户
        key = RedisConstants.LOGIN_USER_KEY + phone;
        stringRedisTemplate.delete(key);
    }

    /**
     * 用户签到
     */
    @Override
    public void sign() {
        // 获取当前登录用户
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            throw new RuntimeException("用户未登录") ;
        }
        Long userId = user.getId();
        // 获取日期
        LocalDateTime now = LocalDateTime.now();
        log.info("用户签到，用户：{}，日期：{}",user,now.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
        // 拼接 key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = RedisConstants.USER_SIGN_KEY + userId + keySuffix; // 每个用户每个月一个 key
        // 获取今天是今天是该月的第几天
        int dayOfMonth = now.getDayOfMonth();
        // 写入 Redis
        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth - 1,true) ;
    }

    /**
     * 用户连续签到次数统计
     *
     * @return
     */
    @Override
    public Integer signCount() {
        // 获取当前登录用户
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            throw new RuntimeException("用户未登录") ;
        }
        Long userId = user.getId();
        // 获取日期
        LocalDateTime now = LocalDateTime.now();
        log.info("用户连续签到次数统计 用户：{}，日期：{}",user,now.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
        // 拼接 key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = RedisConstants.USER_SIGN_KEY + userId + keySuffix; // 每个用户每个月一个 key
        // 获取今天是今天是该月的第几天
        int dayOfMonth = now.getDayOfMonth();
        // 获取本月截止到今天的所有签到记录 返回的是一个十进制数字
        List<Long> result = stringRedisTemplate.opsForValue()
                .bitField(
                        key,
                        BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                                .valueAt(0)
                );
        if (result == null || result.isEmpty()) {
            // 没有签到过
            return 0;
        }
        Long num = result.get(0);
        if (num == null || num == 0) {
            // 没有签到过
            return 0;
        }
        // 循环遍历
        int count = 0 ;
        while (true) {
            // 让这个数字和 1 相与 获取最后一个 bit 位然后左移
            // 判断这个位置是否为 0
            if ((num & 1) == 0) {
                // 如果为 0 说明没签到
                break;
            } else {
                // 不为 0 说明签到了 计数器 + 1
                count ++ ;
            }
        }
        // 数字右移一位
        num >>>= 1 ;
        return count ;
    }

    /**
     * 密码登录
     * @param loginForm
     * @return
     */
    private String loginWithPassword(LoginFormDTO loginForm) {
        // 获取密码
        String password = loginForm.getPassword() ;
        // 获取手机号
        String phone = loginForm.getPhone() ;
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone) && RegexUtils.isEmailInvalid(phone)) {
            // 手机号格式错误
            throw new RuntimeException("手机号格式错误") ;
        }
        // 根据手机号查询用户
        User user = this.query()
                .eq("phone", phone)
                .one();
        if (user == null) {
            throw new RuntimeException("用户不存在") ;
        }
        password = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8)) ;
        // 校验密码
        if (!password.equals(user.getPassword())) {
            throw new RuntimeException("手机号或密码错误") ;
        }
        // 5.将用户保存到 Redis 中
        // 没登陆过就存 phone
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_USER_KEY + loginForm.getPhone(),loginForm.getPhone(),RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
        // 获取 jwt 令牌
        String jwt = jwtUtils.generateJwt(user);
        // 转换为 map 便于存储
        Map<String, Object> userMap = BeanUtil.beanToMap(user,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((field,value)->value.toString()));
        String key = RedisConstants.LOGIN_USER_KEY + jwt;
        stringRedisTemplate.opsForHash().putAll(key,userMap);
        // 添加过期时间
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES) ;
        log.debug("用户登录成功 {}",user);
        // 返回 jwt 令牌
        return jwt;
    }

    /**
     * 创建用户
     * @param phone
     * @return
     */
    private User createUserWithPhone(String phone) {
        String index = String.valueOf(RandomUtil.randomInt(1, 10));
        User user = User.builder()
                .icon("\\imgs\\icons\\" + index + ".jpg")
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .nickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(SystemConstants.USER_NICK_NAME_LENGTH))
                .password(SystemConstants.DEFAULT_USER_PASSWORD)
                .phone(phone)
                .build();
        // 保存用户
        save(user);
        return user;
    }


    /**
     * 发送验证码
     *
     * @param phone
     * @param session
     * @return
     */
    public String sendCodeWithSession(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合 返回错误信息
            throw new RuntimeException("手机号格式错误") ;
        }
        // 3.符合 生成六位验证码
        String code = RandomUtil.randomNumbers(SystemConstants.VERIFY_CODE_LENGTH);
        // 4.保存验证码到 session
        session.setAttribute(RedisConstants.LOGIN_CODE_KEY,code);
        // 5.发送短信验证码
        // TODO 发送验证码
        log.debug("发送验证码成功，验证码为：{}",code);
        // 6.返回
        return code;
    }

    /**
     * 用户登录
     *
     * @param loginForm
     * @param session
     */
    public void loginWithSession(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone() ;
        if (RegexUtils.isPhoneInvalid(phone) && RegexUtils.isEmailInvalid(phone)) {
            // 手机号格式错误
            throw new RuntimeException("手机号格式错误") ;
        }
        // 2.校验验证码
        // 2.1 获取 session 中的验证码
        String cacheCode = (String) session.getAttribute(RedisConstants.LOGIN_CODE_KEY);
        if (cacheCode == null) {
            // 没有发送过验证码
            throw new RuntimeException("请先点击发送验证码") ;
        }
        // 2.2 获取用户提交的验证码
        String code = loginForm.getCode();
        if (RegexUtils.isCodeInvalid(code)) {
            // 验证码格式错误
            throw new RuntimeException("验证码格式错误") ;
        }
        if (!cacheCode.equals(code)) {
            // 3.不一致 返回错误信息
            throw new RuntimeException("验证码错误") ;
        }
        // 4.一致 根据手机号查询用户
        User user = this.query()
                .eq("phone", phone)
                .one();
        if (user == null) {
            // 用户不存在 创建用户并保存到数据库 (自动注册)
            user = this.createUserWithPhone(phone) ;
        }
        // 5.将用户保存到 session 中
        session.setAttribute(SystemConstants.USER_SIGN_KEY,user);
        // 6.移除session中的验证码
        session.removeAttribute(RedisConstants.LOGIN_CODE_KEY);
        log.debug("用户登录成功 {}",user);
    }

}
